#!/usr/bin/env python3

# Copyright (c) 2020-2025 Fpemud <fpemud@sina.com>
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.


import os
import re
import abc
import enum
import pathlib
import itertools
import configparser
import SystemdUnitParser
from ._util import Util


def escape_unit_name(text, path=False, template=None, suffix=None):
    assert text != ""

    if suffix is not None:
        assert not suffix.startswith(".")
        suffix = "." + suffix
    else:
        suffix = ""

    if template is not None:
        assert template.count("@") == 1 and template.endswith("@") and escape_unit_name(text) == text
        return template + escape_unit_name(text) + suffix

    if path:
        assert os.path.isabs(text)
        text = text[1:]                 # remove leading "/"

    escaped = []
    for c in text:
        if c == "/":
            escaped.append("-")
        elif not (c.isalnum() or c in ":-_.\\"):
            escaped.append(f"\\x{c.encode('utf-8').hex()}")            # escape special characters with \xXX hex encoding
        else:
            escaped.append(c)
    return ''.join(escaped) + suffix


def unescape_unit_name(text):
    assert text != ""

    # process hex-encoded sequences like \x2d
    def replace_hex(m):
        hex_str = m.group(1)
        try:
            return bytes.fromhex(hex_str).decode('utf-8')
        except (ValueError, UnicodeDecodeError):
            return m.group(0)  # return original if decoding fails

    # Replace all \xXX sequences with their corresponding characters
    return re.sub(r'\\x([0-9a-fA-F]{2})', replace_hex, text)


class Handy:
    pass


class TargetType(enum.Enum):
    MAIN = enum.auto()
    AUXILLARY = enum.auto()


class ServiceType(enum.Enum):
    DAEMON = enum.auto()
    AUTOMOUNT = enum.auto()
    MOUNT = enum.auto()
    SWAP = enum.auto()


class Unit(abc.ABC):

    def __init__(self, parent, unit_name):
        self._parent = parent

        self._unitName = unit_name

        self._unitFile, _ = self._parent._findUnitFile(self._unitName)
        if self._unitFile is None:
            raise FileNotFoundError("Systemd unit file not found: %s" % (self._unitName))

        self._unitFileContent = pathlib.Path(self._unitFile).read_text()

        self._unitFileData = SystemdUnitParser.SystemdUnitParser()
        self._unitFileData.read_string(self._unitFileContent)

    @property
    def name(self):
        # remove extension
        return self._unitName[:self._unitName.index(".")]

    @property
    def unit_name(self):
        return self._unitName

    @property
    def unit_file_path(self):
        return self._unitFile

    @property
    def unit_file_content(self):
        return self._unitFileContent

    @property
    def description(self):
        return self._getStr("Unit", "Description", True, False)

    @property
    def required_by(self):
        return self._getStrList("Install",  "RequiredBy", True, False)

    @property
    def wanted_by(self):
        return self._getStrList("Install",  "WantedBy", True, False)

    def remove_and_dispose(self):
        Util.forceDelete(self._unitFile)
        del self._unitFileData
        del self._unitFileContent
        del self._unitFile
        del self._unitName
        del self._parent

    def _getStr(self, sectionName, optionName, bSectionMustExist, bOptionMustExist):
        try:
            ret = self._unitFileData.get(sectionName, optionName)
            assert isinstance(ret, str)
            return ret
        except configparser.NoSectionError:
            if bSectionMustExist:
                raise
            return None
        except configparser.NoOptionError:
            if bOptionMustExist:
                raise
            return None

    def _getStrList(self, sectionName, optionName, bSectionMustExist, bOptionMustExist):
        try:
            ret = self._unitFileData.get(sectionName, optionName)
            if isinstance(ret, tuple):
                return list(itertools.chain(*[x.split() for x in ret]))
            elif isinstance(ret, str):
                return ret.split()
            else:
                assert False
        except configparser.NoSectionError:
            if bSectionMustExist:
                raise
            return []
        except configparser.NoOptionError:
            if bOptionMustExist:
                raise
            return []


# class _UnitDataDict(collections.OrderedDict):
#     def __setitem__(self, key, value):
#         if key in ["After", "Before", "RequiredBy", "WantedBy"]:
#             if key not in self:
#                 self[key] = dict()
#             if isinstance(value, str):
#                 value = value.split()
#             elif isinstance(value, dict):
#                 pass
#             else:
#                 assert False
#             for v in value:
#                 self[key][v] = None
#         else:
#             super().__setitem__(key, value)
